<?php
/**
 * CProfileLogRoute class file.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.yiiframework.com/
 * @copyright 2008-2013 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

/**
 * CProfileLogRoute displays the profiling results in Web page.
 *
 * The profiling is done by calling {@link YiiBase::beginProfile()} and {@link YiiBase::endProfile()},
 * which marks the begin and end of a code block.
 *
 * CProfileLogRoute supports two types of report by setting the {@link setReport report} property:
 * <ul>
 * <li>summary: list the execution time of every marked code block</li>
 * <li>callstack: list the mark code blocks in a hierarchical view reflecting their calling sequence.</li>
 * </ul>
 *
 * @property string $report The type of the profiling report to display. Defaults to 'summary'.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @package system.logging
 * @since 1.0
 */
class CProfileLogRoute extends CWebLogRoute
{
    /**
     * @var boolean whether to aggregate results according to profiling tokens.
     * If false, the results will be aggregated by categories.
     * Defaults to true. Note that this property only affects the summary report
     * that is enabled when {@link report} is 'summary'.
     */
    public $groupByToken = true;
    /**
     * @var string type of profiling report to display
     */
    private $_report = 'summary';

    /**
     * Initializes the route.
     * This method is invoked after the route is created by the route manager.
     */
    public function init()
    {
        $this->levels = CLogger::LEVEL_PROFILE;
    }

    /**
     * @return string the type of the profiling report to display. Defaults to 'summary'.
     */
    public function getReport()
    {
        return $this->_report;
    }

    /**
     * @param string $value the type of the profiling report to display. Valid values include 'summary' and 'callstack'.
     * @throws CException if given value is not "summary" or "callstack"
     */
    public function setReport($value)
    {
        if ($value === 'summary' || $value === 'callstack')
            $this->_report = $value;
        else
            throw new CException(Yii::t('yii', 'CProfileLogRoute.report "{report}" is invalid. Valid values include "summary" and "callstack".',
                array('{report}' => $value)));
    }

    /**
     * Displays the log messages.
     * @param array $logs list of log messages
     */
    public function processLogs($logs)
    {
        $app = Yii::app();
        if (!($app instanceof CWebApplication) || $app->getRequest()->getIsAjaxRequest())
            return;

        if ($this->getReport() === 'summary')
            $this->displaySummary($logs);
        else
            $this->displayCallstack($logs);
    }

    /**
     * Displays the callstack of the profiling procedures for display.
     * @param array $logs list of logs
     * @throws CException if Yii::beginProfile() and Yii::endProfile() are not matching
     */
    protected function displayCallstack($logs)
    {
        $stack = array();
        $results = array();
        $n = 0;
        foreach ($logs as $log) {
            if ($log[1] !== CLogger::LEVEL_PROFILE)
                continue;
            $message = $log[0];
            if (!strncasecmp($message, 'begin:', 6)) {
                $log[0] = substr($message, 6);
                $log[4] = $n;
                $stack[] = $log;
                $n++;
            } elseif (!strncasecmp($message, 'end:', 4)) {
                $token = substr($message, 4);
                if (($last = array_pop($stack)) !== null && $last[0] === $token) {
                    $delta = $log[3] - $last[3];
                    $results[$last[4]] = array($token, $delta, count($stack));
                } else
                    throw new CException(Yii::t('yii', 'CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.',
                        array('{token}' => $token)));
            }
        }
        // remaining entries should be closed here
        $now = microtime(true);
        while (($last = array_pop($stack)) !== null)
            $results[$last[4]] = array($last[0], $now - $last[3], count($stack));
        ksort($results);
        $this->render('profile-callstack', $results);
    }

    /**
     * Displays the summary report of the profiling result.
     * @param array $logs list of logs
     * @throws CException if Yii::beginProfile() and Yii::endProfile() are not matching
     */
    protected function displaySummary($logs)
    {
        $stack = array();
        $results = array();
        foreach ($logs as $log) {
            if ($log[1] !== CLogger::LEVEL_PROFILE)
                continue;
            $message = $log[0];
            if (!strncasecmp($message, 'begin:', 6)) {
                $log[0] = substr($message, 6);
                $stack[] = $log;
            } elseif (!strncasecmp($message, 'end:', 4)) {
                $token = substr($message, 4);
                if (($last = array_pop($stack)) !== null && $last[0] === $token) {
                    $delta = $log[3] - $last[3];
                    if (!$this->groupByToken)
                        $token = $log[2];
                    if (isset($results[$token]))
                        $results[$token] = $this->aggregateResult($results[$token], $delta);
                    else
                        $results[$token] = array($token, 1, $delta, $delta, $delta);
                } else
                    throw new CException(Yii::t('yii', 'CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.',
                        array('{token}' => $token)));
            }
        }

        $now = microtime(true);
        while (($last = array_pop($stack)) !== null) {
            $delta = $now - $last[3];
            $token = $this->groupByToken ? $last[0] : $last[2];
            if (isset($results[$token]))
                $results[$token] = $this->aggregateResult($results[$token], $delta);
            else
                $results[$token] = array($token, 1, $delta, $delta, $delta);
        }

        $entries = array_values($results);
        $func = create_function('$a,$b', 'return $a[4]<$b[4]?1:0;');
        usort($entries, $func);

        $this->render('profile-summary', $entries);
    }

    /**
     * Aggregates the report result.
     * @param array $result log result for this code block
     * @param float $delta time spent for this code block
     * @return array
     */
    protected function aggregateResult($result, $delta)
    {
        list($token, $calls, $min, $max, $total) = $result;
        if ($delta < $min)
            $min = $delta;
        elseif ($delta > $max)
            $max = $delta;
        $calls++;
        $total += $delta;
        return array($token, $calls, $min, $max, $total);
    }
}